package it.eng.area118.mdocommon.ntp;

import it.eng.area118.mdocommon.Ntp;
import it.eng.area118.mdocommon.android.log.BasicLogger;
import it.eng.area118.mdocommon.android.util.NumberUtils;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Observer;
import java.util.TimeZone;

public class NtpService implements Ntp {

	private String ntpServer;

	private long maxOffset;

	private int connTimeout;

	private boolean running;

	private NtpObservable observable;

	public NtpService() {
		ntpServer = "";
		maxOffset = 1000;
		connTimeout = 10000;
		observable = new NtpObservable();
	}

	public Calendar getInfo() throws NTPException {
		byte[] ntpData = new byte[48];
		long seventyOffset = 70 * 365; // giorni in 70 anni
		seventyOffset += 17; // giorni degli anni bisestili 1900 - 1970
		seventyOffset *= 24; // ore giornaliere
		seventyOffset *= 60; // minuti in un ora
		seventyOffset *= 60; // secondi in un minuto
		seventyOffset *= 1000; // millisecondi in un secondo

		int ntpPort = 123;
		long transmitMillis = 0;
		long destinationTimestamp;

		// vari offsets in NTPData
		byte refIDOffset = 12;
		// byte referenceOffset = 16;
		byte originateOffset = 24;
		byte receiveOffset = 32;
		byte transmitOffset = 40;

		// resetta i dati
		ntpData[0] = 0x1B;
		for (int i = 1; i < 48; i++) {
			ntpData[i] = 0;
		}

		// orario locale
		long startMillis = System.currentTimeMillis();
		transmitMillis = startMillis + seventyOffset;
		long intPart = transmitMillis / 1000;
		// long fracPart = ( ( transmitMillis % 1000 ) / 1000 ) * 0X100000000L;

		NumberUtils.putLong(ntpData, transmitOffset, intPart);
		NumberUtils.putLong(ntpData, transmitOffset + 4, intPart);

		BasicLogger.getLog().debug("Preparing for ntp connection ...");
		// connessione al server NTP
		DatagramSocket ntpSocket = null;
		try {
			ntpSocket = new DatagramSocket();
			ntpSocket.setSoTimeout(connTimeout);
		} catch (SocketException e) {
			BasicLogger.getLog().error("Impossibile aprire il DatagramSocket",
					e);
			throw new NTPException("Impossibile aprire il DatagramSocket");
		}
		BasicLogger.getLog().debug(
				"connecting to ntp server " + ntpServer + " ...");
		DatagramPacket ntpPacket;
		try {
			ntpPacket = new DatagramPacket(ntpData, ntpData.length,
					InetAddress.getByName(ntpServer), ntpPort);
		} catch (Exception e) {
			BasicLogger.getLog().error(
					"Impossibile connettersi al server NTP " + ntpServer + ".",
					e);
			throw new NTPException("Impossibile connettersi al server NTP "
					+ ntpServer + ". " + e.toString());
		}

		BasicLogger.getLog().debug("sending ntp packet ... ");
		try {
			// invia il pachetto al server NTP
			ntpSocket.send(ntpPacket);
		} catch (Exception e1) {
			BasicLogger.getLog().error("Impossibile inviare il pacchetto NTP.",
					e1);
			throw new NTPException(
					"Impossibile inviare il pacchetto NTP. exception("
							+ e1.toString() + ")");
		}

		BasicLogger.getLog().debug("receiving ntp packet ... ");
		try {
			// riceve il pachetto dal server NTP
			ntpSocket.receive(ntpPacket);
		} catch (Exception e2) {
			BasicLogger.getLog().error(
					"Impossibile ricevere il pacchetto NTP.", e2);
			throw new NTPException(
					"Impossibile ricevere il pacchetto NTP. exception("
							+ e2.toString() + ")");
		}

		BasicLogger.getLog().debug("processing ntp datum");
		// momento in cui e' arrivato il pacchetto dal server NTP
		destinationTimestamp = System.currentTimeMillis();
		destinationTimestamp += seventyOffset;

		// preleva i dati dal pacchetto ricevuto
		ntpData = ntpPacket.getData();

		// info anno bisestile
		byte ntpLeap = (byte) (ntpData[0] >> 6);
		// versione pacchetto ntp
		// byte ntpVersion = ( byte ) ( ( ntpData[0] & 0X38 ) >> 3 );
		// modo
		byte ntpMode = (byte) (ntpData[0] & 0X7);
		// Stratum
		// byte ntpStratum = ( byte ) ( ntpData[1] );

		// verifica pacchetto
		if (ntpLeap == 3) {
			BasicLogger.getLog().error(
					"Il server NTP su indirizzo " + ntpServer
							+ " non e' sincronizzato.");
			throw new NTPException("Il server NTP su indirizzo " + ntpServer
					+ " non e' sincronizzato.");
		}
		if (ntpMode != 4) {
			BasicLogger.getLog().error(
					"Il server NTP su indirizzo " + ntpServer
							+ " non e' abilitato.");
			throw new NTPException("Il server NTP all'indirizzo NTP "
					+ ntpServer + " non e' abilitato.");
		}

		// byte ntpPoll = ( byte ) ( ntpData[2] );
		// Precisione dell'orologio del server (ms).
		// byte ntpPrecision = ( byte ) ( ntpData[3] );
		String refID = "";
		for (int i = 0; i <= 3; i++) {
			refID = refID.concat(String
					.valueOf((char) ntpData[refIDOffset + i]));
		}

		// long referenceTimestamp = retrieveTimestamp( ntpData, referenceOffset
		// );
		long originateTimestamp = retrieveTimestamp(ntpData, originateOffset);
		long receiveTimestamp = retrieveTimestamp(ntpData, receiveOffset);
		long transmitTimestamp = retrieveTimestamp(ntpData, transmitOffset);

		// delay
		if (originateTimestamp != transmitMillis) {
			originateTimestamp = transmitMillis;
		}
		// long rtDelay = ( ( destinationTimestamp - originateTimestamp ) - (
		// receiveTimestamp - transmitTimestamp ) );

		// offset locale
		if (originateTimestamp != transmitMillis) {
			originateTimestamp = transmitMillis;
		}
		long localOffset = ((((receiveTimestamp - originateTimestamp) + (transmitTimestamp - destinationTimestamp))) / 2);

		// chiude la connessione con il server NTP
		try {
			ntpSocket.close();
		} catch (Exception ex) {
			BasicLogger.getLog().error("Error closing ntp socket...", ex);
		}

		NTPDatum ntpDatum = new NTPDatum();
		ntpDatum.setLocalTimestamp(transmitMillis - seventyOffset);
		ntpDatum.setProposedTimestamp(transmitMillis - seventyOffset
				+ localOffset);
		ntpDatum.setOffset(localOffset);

		Calendar c = Calendar.getInstance();
		c.setTimeZone(TimeZone.getTimeZone("Europe/Rome"));
		BasicLogger.getLog().info(
				"Returning NtpDatum: " + ntpDatum.getProposedTimestamp());
		c.setTimeInMillis(ntpDatum.getProposedTimestamp());
		BasicLogger.getLog().info(
				"Synch result: "
						+ new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(c
								.getTime()));

		return c;
	}

	private long retrieveTimestamp(byte[] ntpData, int offset) {
		long intPart = NumberUtils.getLong(ntpData, offset);
		long fracPart = NumberUtils.getLong(ntpData, offset + 4);
		long time = (intPart * 1000) + (fracPart * 1000) / 0X100000000L;
		return time;
	}

	public static long getLong(byte[] data, int offset) {
		return ((((long) data[offset + 0]) & 0xFF) << 24)
				+ ((((long) data[offset + 1]) & 0xFF) << 16)
				+ ((((long) data[offset + 2]) & 0xFF) << 8)
				+ ((((long) data[offset + 3]) & 0xFF));
	}

	public boolean isRunning() {
		return running;
	}

	public void restart() {
	}

	public void resume() {
	}

	public void config(String ip, int connTimeout, long maxOffset) {
		this.ntpServer = ip;
		this.connTimeout = connTimeout;
		this.maxOffset = maxOffset;
	}

	public boolean startup() {
		running = true;
		BasicLogger.getLog().debug("NTP SERVICE started....");
		return running;
	}

	public void suspend() {
	}

	public long getMaxOffset() {
		return maxOffset;
	}

	public void addObserver(Observer observer) {
		if (observer == null) {
			return;
		}
		observable.addObserver(observer);
	}

	public void remObserver(Observer observer) {
		if (observer == null) {
			return;
		}
		observable.deleteObserver(observer);
	}

	public String getNtpServer() {
		return ntpServer;
	}

	public void setNtpServer(String ntpServer) {
		this.ntpServer = ntpServer;
	}

}
